跳到主要内容

自定义绘制器

在 Flutter 中手动推进和绘制 Rive 画板。

你可以使用 CustomPainter 自行管理画板的推进和绘制。这将在绘制层面为你提供更多控制,允许你:

  • 将多个 Rive 画板绘制到同一个 Flutter Canvas
  • 手动推进画板并控制经过的时间。
  • 复用同一个画板实例并多次重绘。
  • 在画布上应用更复杂的裁剪、变换或其他绘制/渲染操作。

Flame 游戏引擎 使用下面讨论的技术来渲染 Rive 动画。此示例中的部分代码取自 flame_rive 包

请注意,这是一个底层 API,在大多数情况下,最好使用 RiveAnimationRive 组件。

示例代码

以下是一个完整示例,演示如何手动推进单个画板并将其在网格中多次绘制到同一个 Flutter 画布上。

参见在线 IDE 示例直接运行。

Image

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:rive/math.dart';
import 'package:rive/rive.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyRiveWidget(),
);
}
}

class MyRiveWidget extends StatefulWidget {
const MyRiveWidget({Key? key}) : super(key: key);

@override
State<MyRiveWidget> createState() => _MyRiveWidgetState();
}

class _MyRiveWidgetState extends State<MyRiveWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 10));
RiveArtboardRenderer? _artboardRenderer;

Future<void> _load() async {
// 你需要自行管理将控制器添加到画板,
// 不像 RiveAnimation 组件那样通过简单地提供状态机(或动画)名称来处理大量此类逻辑。
final file = await RiveFile.asset('assets/little_machine.riv');
final artboard = file.mainArtboard.instance();
final controller = StateMachineController.fromArtboard(
artboard,
'State Machine 1',
);
artboard.addController(controller!);
setState(
() => _artboardRenderer = RiveArtboardRenderer(
antialiasing: true,
fit: BoxFit.cover,
alignment: Alignment.center,
artboard: artboard,
),
);
}

@override
void initState() {
super.initState();
_animationController.repeat();
_load();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _artboardRenderer == null
? const SizedBox()
: CustomPaint(
painter: RiveCustomPainter(
_artboardRenderer!,
repaint: _animationController,
),
child: const SizedBox.expand(), // 使用所有可用空间
),
),
);
}
}

class RiveCustomPainter extends CustomPainter {
final RiveArtboardRenderer artboardRenderer;

RiveCustomPainter(this.artboardRenderer, {super.repaint}) {
_lastTickTime = DateTime.now();
_elapsedTime = Duration.zero;
}

late DateTime _lastTickTime;
late Duration _elapsedTime;

void _calculateElapsedTime() {
final currentTime = DateTime.now();
_elapsedTime = currentTime.difference(_lastTickTime);
_lastTickTime = currentTime;
}

@override
void paint(Canvas canvas, Size size) {
_calculateElapsedTime(); // 计算自上次 tick 后经过的时间。

// 按经过的时间推进画板。
artboardRenderer.advance(_elapsedTime.inMicroseconds / 1000000);

final width = size.width / 3;
final height = size.height / 2;
final artboardSize = Size(width, height);

// 第一行
canvas.save();
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width, 0);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width * 2, 0);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();

// 第二行
canvas.save();
canvas.translate(0, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width * 2, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();

// 绘制完整画布大小
// artboardRenderer.render(canvas, size);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

/// 保存 `Artboard` 实例并将其渲染到 `Canvas`。
///
/// 这是 `RiveAnimation` 组件及其 RenderObject 的简化版本
///
/// 它考虑了 `fit` 和 `alignment` 属性,类似于 `RiveAnimation` 的工作方式。
class RiveArtboardRenderer {
final Artboard artboard;
final bool antialiasing;
final BoxFit fit;
final Alignment alignment;

RiveArtboardRenderer({
required this.antialiasing,
required this.fit,
required this.alignment,
required this.artboard,
}) {
artboard.antialiasing = antialiasing;
}

void advance(double dt) {
artboard.advance(dt, nested: true);
}

late final aabb = AABB.fromValues(0, 0, artboard.width, artboard.height);

void render(Canvas canvas, Size size) {
_paint(canvas, aabb, size);
}

final _transform = Mat2D();
final _center = Mat2D();

void _paint(Canvas canvas, AABB bounds, Size size) {
const position = Offset.zero;

final contentWidth = bounds[2] - bounds[0];
final contentHeight = bounds[3] - bounds[1];

if (contentWidth == 0 || contentHeight == 0) {
return;
}

final x = -1 * bounds[0] -
contentWidth / 2.0 -
(alignment.x * contentWidth / 2.0);
final y = -1 * bounds[1] -
contentHeight / 2.0 -
(alignment.y * contentHeight / 2.0);

var scaleX = 1.0;
var scaleY = 1.0;

canvas.save();
canvas.clipRect(position & size);

switch (fit) {
case BoxFit.fill:
scaleX = size.width / contentWidth;
scaleY = size.height / contentHeight;
break;
case BoxFit.contain:
final minScale =
min(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = minScale;
break;
case BoxFit.cover:
final maxScale =
max(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = maxScale;
break;
case BoxFit.fitHeight:
final minScale = size.height / contentHeight;
scaleX = scaleY = minScale;
break;
case BoxFit.fitWidth:
final minScale = size.width / contentWidth;
scaleX = scaleY = minScale;
break;
case BoxFit.none:
scaleX = scaleY = 1.0;
break;
case BoxFit.scaleDown:
final minScale =
min(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = minScale < 1.0 ? minScale : 1.0;
break;
}

Mat2D.setIdentity(_transform);
_transform[4] = size.width / 2.0 + (alignment.x * size.width / 2.0);
_transform[5] = size.height / 2.0 + (alignment.y * size.height / 2.0);
Mat2D.scale(_transform, _transform, Vec2D.fromValues(scaleX, scaleY));
Mat2D.setIdentity(_center);
_center[4] = x;
_center[5] = y;
Mat2D.multiply(_transform, _transform, _center);

canvas.translate(
size.width / 2.0 + (alignment.x * size.width / 2.0),
size.height / 2.0 + (alignment.y * size.height / 2.0),
);

canvas.scale(scaleX, scaleY);
canvas.translate(x, y);

artboard.draw(canvas);
canvas.restore();
}
}

RiveArtboardRenderer 类取自 Flame Rive 包,可以作为理解 Rive 如何使用 AlignmentBoxFit 将画板布局到画布上的起点。

关键步骤如下:

  1. RiveFile 访问画板并附加任何 Rive 动画控制器(StateMachineController)。动画可以像往常一样通过控制器进行控制。
  2. 创建 Flutter CustomPaint 组件以访问 Flutter 画布。
  3. 使用 AnimationController(或 Ticker/Listener)强制 RiveCustomPainter 在每一帧重新绘制。
  4. 计算动画 tick 之间的经过时间。
  5. 使用 artboard.advance(dt, nested: true); 推进画板,其中 dt 是经过的时间(增量时间)。
  6. 使用 artboard.draw(canvas); 将画板绘制到画布上。

其余代码负责布局和尺寸调整。

其他示例

在此示例中使用了单个画板,但也可以将多个画板实例(来自相同或不同的 Rive 文件)绘制到同一个画布上。

参见此可编辑示例,它展示了如何将两个不同的画板(为每个僵尸创建唯一的画板实例)绘制到同一画布上。每个画板都有一个数字输入,用于在不同皮肤之间切换:

Rive Flutter 自定义绘制器 - 多个画板:

Image

请注意,在画板上调用 .instance() 会创建一个可以独立推进的唯实例。